Skip to content

feat(hooks): add Claude Code pre-compact snapshot hook#226

Open
Gradata wants to merge 1 commit into
mainfrom
feat/gra-1210-pre-compact
Open

feat(hooks): add Claude Code pre-compact snapshot hook#226
Gradata wants to merge 1 commit into
mainfrom
feat/gra-1210-pre-compact

Conversation

@Gradata
Copy link
Copy Markdown
Owner

@Gradata Gradata commented May 26, 2026

Summary

  • add Claude Code PreCompact snapshot hook callable via python -m gradata.hooks.pre_compact
  • write bounded brain context snapshots to <brain>/.precompact-snapshots/<session-id>.json
  • wire gradata install --agent claude-code to install PreCompact alongside PreToolUse
  • update stale intelligence coverage and add focused PreCompact tests

Verification

  • python3 -m pytest tests/test_pre_compact_hook.py tests/test_hook_adapters.py::test_claude_code_install_writes_pre_compact_entry tests/test_hooks_intelligence.py::test_pre_compact_saves_snapshot tests/test_hooks_intelligence.py::test_pre_compact_no_brain -q — 7 passed
  • python3 -m pytest tests/test_hook_adapters.py tests/test_pre_compact_hook.py tests/test_hooks_intelligence.py tests/test_jit_inject.py tests/test_session_history.py -q — 87 passed, 1 skipped

Closes GRA-1210. Parent epic: GRA-1198 / GH #206.

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

Review Change Stack

📝 Walkthrough
  • Adds Claude Code PreCompact snapshot hook: Writes bounded brain context snapshots to <brain>/.precompact-snapshots/<session-id>.json with deterministic session IDs and context limits
  • New command-construction helpers: Adds auto_correct_command(), session_close_command(), and pre_compact_command() to hook adapters base module
  • Integrated Claude Code installation: gradata install --agent claude-code now provisions both PreToolUse and PreCompact hooks with idempotent behavior
  • Profile optimization: PreCompact hook profile downgraded from STANDARD to MINIMAL for performance
  • ⚠️ BREAKING CHANGE: pre_compact.main() return type changed from dict | None to None
  • Enhanced snapshot metadata: Captures hook event name, session ID, trigger, CWD, transcript path, and custom instructions with explicit size limit tracking
  • Path traversal protection: Session IDs are sanitized to prevent malicious filenames with .. or / characters
  • Graceful degradation: Hook operates as no-op when brain directory is missing
  • Comprehensive test coverage: Added 4 focused tests for PreCompact hook plus new adapter test verifying hook installation

Walkthrough

This PR adds a new Pre-Compact hook that captures bounded Gradata context snapshots. The hook integrates into the Claude Code adapter's lifecycle management, writing deterministic JSON snapshots before compaction. Command construction helpers, snapshot schema and persistence logic, adapter install/uninstall support, and comprehensive validation tests are all included.

Changes

Pre-Compact Hook Feature

Layer / File(s) Summary
Hook command construction infrastructure
Gradata/src/gradata/hooks/adapters/_base.py
Adds auto_correct_command(), session_close_command(), and pre_compact_command() helpers that build shell command strings setting BRAIN_DIR and invoking the corresponding python -m gradata.hooks.* modules.
Pre-Compact hook snapshot implementation
Gradata/src/gradata/hooks/pre_compact.py
Replaces prior snapshot logic with deterministic session ID hashing, bounded UTF-8 file reads from brain_prompt.md and .last_injection.json, and atomic JSON writes to .precompact-snapshots/<session_id>.json. Changes HOOK_META profile to MINIMAL and signature to main(data: dict[str, Any]) -> None.
Claude Code adapter PreCompact integration
Gradata/src/gradata/hooks/adapters/claude_code.py, Gradata/tests/test_hook_adapters.py
Extends install() to provision both PreToolUse and PreCompact hooks via pre_compact_command(), validates idempotency when both exist, appends missing blocks, and updates uninstall() to remove both hook types with safe list pruning. New test verifies PreCompact entry writing to settings.
Hook behavior and integration tests
Gradata/tests/test_hooks_intelligence.py, Gradata/tests/test_pre_compact_hook.py
Updated intelligence test validates deterministic snapshot path output and event/context metadata. New test module adds four tests: snapshot writing with brain prompt content, session ID sanitization against path traversal, no-op for missing brain directory, and end-to-end invocation via run_hook().

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Gradata/gradata#215: Both PRs modify Gradata/src/gradata/hooks/adapters/claude_code.py to update the Claude Code adapter's uninstall logic—this PR extends it to remove PreCompact entries alongside PreToolUse, while the related PR introduces hook signature entry handling for the uninstall command.

Suggested labels

feature

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately describes the main change: adding a Claude Code pre-compact snapshot hook feature.
Description check ✅ Passed The description is clearly related to the changeset, providing a summary of the feature additions, implementation details, and verification results.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/gra-1210-pre-compact

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 OpenGrep (1.22.0)

OpenGrep fatal error (exit code 2):
┌──────────────┐
│ Opengrep CLI │
└──────────────┘

�[32m✔�[39m �[1mOpengrep OSS�[0m
�[32m✔�[39m Basic security coverage for first-party code vulnerabilities.

�[1m Loading rules from local config...�[0m
[00.33][ERROR]: Error: exception Glob.Lexer.Syntax_error("malformed glob pattern: missing ']'")
Raised at Glob__Lexer.syntax_error in file "libs/glob/Lexer.mll", line 8, characters 2-26
Called from Glob__Lexer.__ocaml_lex_token_rec in file "libs/glob/Lexer.mll", line 29, characters 26-53
Cal


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the feature label May 26, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Gradata/src/gradata/hooks/pre_compact.py`:
- Around line 118-121: The code only checks brain_dir.exists() before
proceeding, which still allows a file path to pass and later fail when creating
snapshot directories; update the guard to ensure brain_dir is a directory (use
brain_dir.is_dir()) and return None if it's not a directory, then proceed to
call _session_id(data), _build_snapshot(brain_dir, data), and
_write_snapshot(_snapshot_path(brain_dir, session_id), ...) only when
brain_dir.is_dir() is True to prevent write errors.

In `@Gradata/tests/test_hook_adapters.py`:
- Around line 88-89: The two asserts check fragments separately and can match
different entries; change them to assert that a single command string contains
both fragments by replacing the two asserts with one that uses any(...) over
commands, e.g. any("BRAIN_DIR=" in cmd and "gradata.hooks.pre_compact" in cmd
for cmd in commands), so update the assertions in test_hook_adapters.py to
ensure both substrings appear in the same command entry.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1ef769ee-2ad4-47b9-a0cb-2474b89f0d15

📥 Commits

Reviewing files that changed from the base of the PR and between a197bff and 472bd05.

📒 Files selected for processing (6)
  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/claude_code.py
  • Gradata/src/gradata/hooks/pre_compact.py
  • Gradata/tests/test_hook_adapters.py
  • Gradata/tests/test_hooks_intelligence.py
  • Gradata/tests/test_pre_compact_hook.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: pytest (py3.11)
  • GitHub Check: pytest windows-latest / py3.12
  • GitHub Check: pytest ubuntu-latest / py3.11
  • GitHub Check: pytest macos-latest / py3.11
  • GitHub Check: pytest windows-latest / py3.11
  • GitHub Check: pytest ubuntu-latest / py3.12
  • GitHub Check: pytest macos-latest / py3.12
🧰 Additional context used
📓 Path-based instructions (2)
Gradata/tests/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/tests/**/*.py: Set BRAIN_DIR environment variable via tmp_path in conftest.py for test isolation — ensure _paths.py module cache refreshes when calling Brain.init() directly inside tests
Add unit tests in tests/test_*.py for every CI push without LLM calls (deterministic); mark integration tests with @pytest.mark.integration and skip them by default (they hit real LLM APIs)

Files:

  • Gradata/tests/test_hook_adapters.py
  • Gradata/tests/test_hooks_intelligence.py
  • Gradata/tests/test_pre_compact_hook.py
Gradata/src/**/*.py

📄 CodeRabbit inference engine (Gradata/AGENTS.md)

Gradata/src/**/*.py: Prefer sentence-transformers for local embeddings, google-genai for Gemini embeddings, cryptography for AES-GCM encrypted system.db, bm25s for BM25 rule ranking, and mem0ai for external memory adapters — guard all optional dependency imports with try / except ImportError at the call site, never at module level
Maintain strict layering: Layer 0 (Primitives: _types.py, _db.py, _events.py, _paths.py, _file_lock.py; Patterns: contrib/patterns/) must never import from Layer 1 (Enhancements: enhancements/, rules/) or Layer 2 (Public API: brain.py, cli.py, daemon.py, mcp_server.py)
Never use bare except: pass — use typed exceptions or at minimum logger.warning(...) with exc_info=True to avoid silent failure in a memory product
Never import from out-of-scope sibling directories ../Sprites/ or ../Hausgem/ within gradata/* code — that is a layering bug
Never leak private-sibling paths into public docs/code — no references to ../Sprites/, ../Hausgem/, email addresses, OneDrive paths, or Sprites-specific examples from inside gradata/*
Use atomic-write helper when writing JSON files to prevent corruption from mid-write crashes

Files:

  • Gradata/src/gradata/hooks/adapters/_base.py
  • Gradata/src/gradata/hooks/adapters/claude_code.py
  • Gradata/src/gradata/hooks/pre_compact.py
🔇 Additional comments (6)
Gradata/src/gradata/hooks/adapters/_base.py (1)

139-157: LGTM!

Gradata/src/gradata/hooks/pre_compact.py (1)

22-117: LGTM!

Also applies to: 122-123

Gradata/src/gradata/hooks/adapters/claude_code.py (1)

15-18: LGTM!

Also applies to: 62-97, 120-137

Gradata/tests/test_hook_adapters.py (1)

3-3: LGTM!

Also applies to: 69-87

Gradata/tests/test_hooks_intelligence.py (1)

281-313: LGTM!

Gradata/tests/test_pre_compact_hook.py (1)

10-73: LGTM!

Comment on lines +118 to +121
if not brain_dir.exists():
return None
session_id = _session_id(data)
_write_snapshot(_snapshot_path(brain_dir, session_id), _build_snapshot(brain_dir, data))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against file-path BRAIN_DIR before writing snapshots.

Line 118 only checks existence. If BRAIN_DIR points to a file, Line 121 can fail during snapshot directory creation.

Suggested fix
 def main(data: dict[str, Any]) -> None:
     resolved = resolve_brain_dir()
     if not resolved:
         return None
     brain_dir = Path(resolved)
-    if not brain_dir.exists():
+    if not brain_dir.is_dir():
         return None
     session_id = _session_id(data)
     _write_snapshot(_snapshot_path(brain_dir, session_id), _build_snapshot(brain_dir, data))
     return None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not brain_dir.exists():
return None
session_id = _session_id(data)
_write_snapshot(_snapshot_path(brain_dir, session_id), _build_snapshot(brain_dir, data))
if not brain_dir.is_dir():
return None
session_id = _session_id(data)
_write_snapshot(_snapshot_path(brain_dir, session_id), _build_snapshot(brain_dir, data))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/src/gradata/hooks/pre_compact.py` around lines 118 - 121, The code
only checks brain_dir.exists() before proceeding, which still allows a file path
to pass and later fail when creating snapshot directories; update the guard to
ensure brain_dir is a directory (use brain_dir.is_dir()) and return None if it's
not a directory, then proceed to call _session_id(data),
_build_snapshot(brain_dir, data), and _write_snapshot(_snapshot_path(brain_dir,
session_id), ...) only when brain_dir.is_dir() is True to prevent write errors.

Comment on lines +88 to +89
assert any("BRAIN_DIR=" in command for command in commands)
assert any("gradata.hooks.pre_compact" in command for command in commands)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert both fragments on the same command entry.

Line 88 and Line 89 can pass even if BRAIN_DIR= and gradata.hooks.pre_compact appear in different commands.

Suggested fix
-    assert any("BRAIN_DIR=" in command for command in commands)
-    assert any("gradata.hooks.pre_compact" in command for command in commands)
+    assert any(
+        "BRAIN_DIR=" in command and "gradata.hooks.pre_compact" in command
+        for command in commands
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
assert any("BRAIN_DIR=" in command for command in commands)
assert any("gradata.hooks.pre_compact" in command for command in commands)
assert any(
"BRAIN_DIR=" in command and "gradata.hooks.pre_compact" in command
for command in commands
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Gradata/tests/test_hook_adapters.py` around lines 88 - 89, The two asserts
check fragments separately and can match different entries; change them to
assert that a single command string contains both fragments by replacing the two
asserts with one that uses any(...) over commands, e.g. any("BRAIN_DIR=" in cmd
and "gradata.hooks.pre_compact" in cmd for cmd in commands), so update the
assertions in test_hook_adapters.py to ensure both substrings appear in the same
command entry.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant